package crystal.workshop.ui;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.table.AbstractTableModel;


/**
 * Adapts a structures list to a table model so it can be easily used by
 * classes such as <code>JTable</code>.
 * 
 * @author Martin Uhrin.
 *
 */
public class StructuresListModelTableAdapter extends AbstractTableModel {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1106707659974284213L;
	
	class AdapterController implements ListDataListener, PropertyChangeListener {
		
		/** PropertyChangeListener interface **/
		@SuppressWarnings(value = "unchecked")
		public void propertyChange(final PropertyChangeEvent evt) { 
			if(evt.getSource() == strListModel && evt.getPropertyName().equals("structures")) {
				// Have some structure been removed?
				if(evt.getOldValue() != null && evt.getNewValue() == null) {
					structuresRemoved((Collection<StructureDataModel>)evt.getOldValue());
				}
			} else if(evt.getSource().getClass().equals(StructureDataModel.class) &&
					evt.getPropertyName().equals("structureProperty")) {
				// TODO: Add code to handle the adding and removing of structure properties here
				if(evt.getOldValue() == null && evt.getNewValue()!= null) {
					structurePropertyAdded((StructureDataModel)evt.getSource(), (String)evt.getNewValue());
				} else if(evt.getOldValue() != null && evt.getNewValue() == null) {
					structurePropertyRemoved((StructureDataModel)evt.getSource(), (String)evt.getOldValue());
				}
			}
		}
		
		/** ListDataListener interface **/
		public void contentsChanged(final ListDataEvent e) {
			if(e.getSource() == StructuresListModelTableAdapter.this.strListModel)
				listContentsChanged();
		}

		public void intervalAdded(final ListDataEvent e) {
			if(e.getSource() == StructuresListModelTableAdapter.this.strListModel)
				structuresAdded(e.getIndex0(), e.getIndex1());
		}
		public void intervalRemoved(final ListDataEvent e) {
			if(e.getSource() == StructuresListModelTableAdapter.this.strListModel)
				structuresRemoved(e.getIndex0(), e.getIndex1());
		}
	};
	
	/** The structure list backing this table */
	protected StructuresListModel strListModel;
	
	protected final AdapterController controller = new AdapterController();
	
	protected ArrayList<String> columnNames = new ArrayList<String>();
	protected ArrayList<Class<?>> columnTypes	= new ArrayList<Class<?>>();
	
	public StructuresListModelTableAdapter(final StructuresListModel strListModel) {
		setStructureListModel(strListModel);
		
		// Set up all the connections
		initialise();
	}
	
	@Override
	public int getColumnCount() { 
		return columnTypes.size();
	}
	
	@Override
	public int getRowCount() {
		return strListModel.getStructures().size();
	}

	@Override
	public Class<?> getColumnClass(final int columnIndex) {
		return columnTypes.get(columnIndex);
	}
	
	@Override
	public Object getValueAt(final int row, final int column) {
		// TODO: Check for valid row, column
		final StructureDataModel str = strListModel.getStructure(row);
		Object value = null;
		if(column == 0){
			value = str.getCustomName();
		} else {
			value = str.getProperty(columnNames.get(column)); 
		}
		return value; 
	}
	
	@Override
	public String getColumnName(final int col) {
		return columnNames.get(col);
	}
	
	@Override
	public boolean isCellEditable(final int rowIndex, final int columnIndex) {
		// Only the name column is editable
		if(columnIndex == 0) {
			return true;
		}
		return false;
	}
	
	/**
	 * Return the structure list model.
	 * @return the structures list model.
	 */
	protected StructuresListModel getStructuresListModel() {
		return strListModel;
	}
	
	private void initialise() {
	}
	
	/**
	 * Called when new structures are added to the structure list model.
	 * @param index0 the index of the first structure added
	 * @param index1 the index of the last structure added (inclusive)
	 */
	protected void structuresAdded(final int index0, final int index1) {
			
		boolean columnCreated = false;
		Map.Entry<String, Object> property;
		for(int i = index0; i <= index1; ++i) {
			for(Iterator<Map.Entry<String,Object>> iter = strListModel.getStructure(i).getProperties().entrySet().iterator();
			iter.hasNext();) {
				property = iter.next();
				// Check if we need to create any new columns
				if(!columnNames.contains(property.getKey())) {
					createColumnForProperty(property.getKey(), property.getValue().getClass());
					columnCreated = true;
				}
			}
			strListModel.addPropertyChangeListener(controller);
		}
		// Have we create any new columns?
		if(columnCreated) {
			fireTableStructureChanged();
		} else {
			fireTableRowsInserted(index0, index1);
		}
	}
	
	/**
	 * Called when structures are removed from the structure list model.
	 * @param index0 the index of the first structure added
	 * @param index1 the index of the last structure added (inclusive)
	 */
	protected void structuresRemoved(final int index0, final int index1) {
		calculateDefaultColumns();
		//fireTableRowsDeleted(index0, index1);
		// TODO: Need to set this up so it only fires a structure changed if we
		// changed the columns, otherwise it fires a table rows deleted
		fireTableStructureChanged();
	}
	
	/**
	 * Called when structures are removed from the structure list model.
	 * Crucially the structure list sends two messages when this happens: one
	 * to all list data listeners and one to all property listeners.  The
	 * latter contains a collection of the removed structures as without these
	 * there is no way to access the deleted structures anymore.
	 *  
	 * @param structures the collection of structures that was removed
	 */
	protected void structuresRemoved(final Collection<StructureDataModel> structures) {
		for(StructureDataModel str : structures) {
			str.removePropertyChangeListener(controller);
		}
	}
	
	protected void structurePropertyAdded(final StructureDataModel str, final String propertyName) {
		if(!columnNames.contains(propertyName)) {
			createColumnForProperty(propertyName, str.getProperty(propertyName).getClass());
			fireTableStructureChanged();
		}
	}
	
	protected void structurePropertyRemoved(final StructureDataModel str, final String propertyName) {
		calculateDefaultColumns();
		fireTableStructureChanged();
	}
	
	/**
	 * The contents of the list has changed either significantly or completely
	 * and we should recalculate the table.
	 */
	protected void listContentsChanged() {
		calculateDefaultColumns();
		fireTableStructureChanged();
	}

	protected void createColumnForProperty(final String name, final Class<?> type) {
		columnNames.add(name);
		columnTypes.add(type);
	}
	
	protected void calculateDefaultColumns() {
		resetColumns();
		for(int i = 0; i < strListModel.getSize(); ++i) {
			Map.Entry<String, Object> property;
			for(Iterator<Map.Entry<String,Object>> iter = strListModel.getStructure(i).getProperties().entrySet().iterator();
			iter.hasNext();) {
				property = iter.next();
				// Check if we need to create any new columns
				if(!columnNames.contains(property.getKey())) {
					createColumnForProperty(property.getKey(), property.getValue().getClass());
				}
			}
		}
	}
	
	/**
	 * Reset columns back to their default state.
	 */
	protected void resetColumns() {
		columnNames.clear();
		columnTypes.clear();
		columnNames.add("Name");
		columnTypes.add(String.class);
		fireTableStructureChanged();
	}
	
	/**
	 * Set the the structure list model to a new value.
	 * @param newValue the new structure list model
	 */
	public void setStructureListModel(final StructuresListModel newValue) {
		if (strListModel != newValue) {
			try {
				//final StructuresListModel oldValue = getStructuresListModel();
				/* Stop listening for events from the current object */
				if (strListModel != null) {
					strListModel.removeListDataListener(controller);
				}
				strListModel = newValue;
	
				/* Listen for events from the new object */
				if (strListModel != null) {
					strListModel.addListDataListener(controller);
				}
				
				// Regenerate our columns
				resetColumns();
				listContentsChanged();
				
				
				//firePropertyChange(Property.LIST_MODEL, oldValue, newValue);
			} catch (Exception e) {
			}
		};
	}
}
